作者简介
Sheila,携程资深前端开发工程师,关注前端性能优化;xqin,携程前端开发专家,CRN bundle分析平台开发者。
在业务迭代上线的过程中,往往会出现一些代码冗余,导致最终打包出来的 bundle size 不尽如人意。同时,业务包占用的尺寸过大,对应用的性能以及用户体验都会造成一定程度的影响。
本文将从 JavaScript 层面对 React Native 的业务包进行分析与优化,在这个过程中会运用 CRN (Ctrip React Native)bundle 分析平台等工具,在项目开发的中后期对业务包的尺寸进行裁剪优化。
目前针对 React Native 的性能调优可以使用的工具少之又少,下面将介绍 React Native 中可以对 bundle 进行可视化的本地工具,以及我们为什么需要一个在线平台去构建 bundle 分析结果。
2.1 使用 bundle-analyzer 进行包模块内容的实时查看
Metro 是 React Native 官方的打包程序,会生成对应的 bundle 文件。在 react 中或者是使用 webpack 等工具打包出来的内容,都可以使用与 source-map-explorer 相关的一些打包分析工具进行可视化内容查看。
使用 bundle 分析工具,可以比较明显地辨识出哪些业务文件大小比较异常、需要进行优化,或者是引用了哪些 Javascript 库,导致 bundle 膨胀。
cmd yarn add --dev react-native-bundle-visualizer && yarn run react-native-bundle-visualizer
如果没有跑出具体内容,则需要手动添加入口文件。例如,--entry-file ./index.android.js。执行结果会把 node_modules 和源文件中打包出来的代码尺寸都包含在内,可以清晰地看出哪些文件占用的空间比较大。
2.2 为什么要开发 CRN bundle 分析平台
Web 端针对 React 的分析优化工具很多,包括 webpack 官方也有提供打包分析,但这些针对 React Native 都不能使用。在上一小节中提到的工具,也只能在本地运行,每次改动后需要生成新的 treemap 进行图片之间的对比查看,不直观并且不方便对比。React Native 开发的模块最后都会打包到 APP 中,如果能在平时的开发阶段,就注重保持 Bundle SIZE 的简洁,注意观察业务包 SIZE 的限制大小,那么不需要后期进行排查裁剪。CRN bundle 分析平台不需要使用者手动运行,只需要使用者选择自己的业务包名称,即可进行在线的分析,并且可生成过去7天 bundle size 色阶图,可以让使用者对过去一段时间内的开发打包结果进行及时排查,也就是说可以对包内尺寸的膨胀进行告警。现有的 React Native Bundle 分析工具,除了只能本地进行运行以外,还存在的缺点就是它是针对 React Native 官方的打包工具的运行结果进行的分析,对于 Ctrip React Native 或者是其他基于 React Native 优化的跨平台开发框架,是会有一定缺陷的,例如无法找到正确的入口文件、无法找到对应的依赖关系等等。针对 React Native 进行 bundle 分析的在线平台,相较于现有的工具,具有以下优点:- Ctrip React Native Bundler 打包结果定制化分析
定制化的 RN bundle 分析平台,可以随时拉取当前业务包的历史打包结果,并且进行在线分析与告警,还可以让使用者得到一个关于本次优化内容的文件差异对比内容,在分析优化后,可以快速看到优化效果,简单高效。
CRN bundle 分析平台,可以对 React Native 打包后的内容进行在线二次分析。它具有项目内部模块依赖分析、文件尺寸树状结构矩形图等图表展示功能。
3.1.1 bundle 概要
如上所示中,展示了业务模块的整体大小以及压缩后的尺寸,并且进行了图形化的占比展示。在条形图中,从打包的模块内容角度,显示了当前业务包中占比最大的五项内容,包括 build 后生成的内容,以及 node_modules 中的模块大小占比。底部的占比图中,从文件类型的角度,显示了当前业务包中的 JavaScript、Font、Image 等文件类型的大小占比。在 bundle 概要的页面,显示了当前业务包的源代码大小以及打包后的压缩大小。
3.1.2 SIZE 详情
bundle Size 详情页面,使用树形结构图,直观地展示了当前业务包中各个模块的尺寸大小以及占比。可对相应的模块文件进行搜索查看,同时会高亮展示在树形结构图区域,以便排查和优化打包结果。使用详情页面,可以对优化前后的结果进行图形化的尺寸对比。
3.1.3 模块依赖分析
模块依赖分析页面,会根据模块的依赖关系生成 dependency graph,便于排查模块之间的深层依赖。
3.1.4 文件差异比较
对于任意两个发布单,可以根据 JobId 进行包内各个文件的大小 diff 对比,并且会链接到 gitlab 对应的 changes 内容,可以看到代码优化部分的相关 commmits 。使用该功能,可以直观地进行某次裁剪前后的尺寸大小对比,快速验证优化效果。
3.1.5 CRN 模块可用包大小统计
对于每个业务包可以给出一个可用包尺寸大小,并且根据每日打包结果,生成对应的过去时间段中的打包尺寸大小色阶图,使用色阶可以预警过去的时间段中是否出现超限的业务包打包结果,及时对打包内容进行排查。
CRN bundle 分析平台主要依赖三个部分进行实现,分别是处理 JOB 数据、使用后台 API 分析打包后的业务包文件,最后在前端进行各种图表化的展示。
打开平台页面后,使用者选择要分析的业务包名称,后台API根据参数调用相关接口,得到要分析的业务包的下载地址和对应的内容映射文件,并且将数据添加到队列中,等待后续分析处理。循环调用后台 API 去获取要分析的 JOB 进行数据处理。在这个过程中,调用 Nodejs 对当前选择的业务包进行基础分析,并与 map 文件相结合,得到关键依赖数据与代码详情内容,生成最基础也是最重要的数据包,这个数据包使用 JobId 作为文件名称,得到一个 JSON 格式的数据内容,后续的处理都在这个 JSON 文件的基础上进行。获得一个基础数据文件后,使用前端把数据处理为需要的格式进行展示。在前端交互逻辑上使用了 Vue.js 与 element-ui 进行基础页面构建,使用 d3.js 进行了数据的可视化展示,在这里用到了树形矩阵图、色阶图、条形图、依赖关系图等等图表进行内容展示。在 DIFF 页面中,同时分析了两个指定的 JobId 下的业务包内容,并且按照差异内容进行了详细的 SIZE 增减对比。
在进行包裁剪之前,我们需要先分析业务包内各模块的占比大小,以便对具体的模块进行修改。工欲善其事,必先利其器。在有了分析工具后,可以对业务包模块进行详细的分析与优化。在这里,使用本地安装的 bundle 分析工具进行普适的分析。在这个截图中,可以很清楚地看到,除了公共引用库以外的内容中,有几个比较明显的膨胀模块,分别是 lodash、moment,以及一个工具类库下的业务逻辑文件。接下来我们针对这几处明显的问题进行优化。
Momentjs 和 Lodashjs 是前端常用类库,但这两个都有很明显的问题,所占据的文件空间略大,而且大多数时候我们只需要用到其中小部分的功能。在如下类库替换过程中用到的方法,可以运用到所有常用类库的优化使用中。
5.1.1 选择满足需求的最小类库
moment 是一个常用的JavaScript日期处理类库,它支持多语言的日期格式。moment 的核心代码只有52kb,但是包含了全世界语言的本地化文件,也就是说当你使用其中的功能时,也包含了很多你用不到的特性。对应的解决方案是你可以通过 npm 安装moment-mini,该库非官方维护,但暴露了官方的 moment-min.js 作为 npm 模块开源使用。或者你可以直接使用一些更为简洁的 JavaScript 日期格式化类库。作为 momentjs 的替代方案,可以使用 luxon、date-fns、dayjs,或者直接使用 JavaScript 的原生 API 来做日期国际化(JavaScript Internationalization API)。如果不需要引入日期国际化,dayjs 核心代码只有7.1k,可以作为 momentjs 的替代。
5.1.2 不必要时避免引入整个类库
lodash 是一个实用性非常高的 JavaScript 工具库,可以对 array、object、string 等值进行操作和检测等等,还具有一些非常实用的函数。但lodash类库所占用的空间达到了71K,而且也存在很多你用不上的方法。实际上,我们在使用中或许只会用到非常少的几个函数。官方虽然也提供了 lodash-cli 这样的工具,让使用方可以针对具体的某些函数进行打包,但官方是不推荐这种用法的,并且在新的版本中也取消了这样的部分模块打包方式。官方推荐的方式是,在引用时指定对应的函数,这样最终打包时只会打包对应的函数。如下所示,如果直接引用 lodash,大小时71K。javascript import get from 'lodash' // 71K (gzipped: 24.7K)
javascript import get from 'lodash/get' // 8.2K (gzipped: 2.5K)
通过 Babel 插件配置
# .babelrc
"plugins": [
["transform-imports", {
"lodash": {
"transform": "lodash/${member}",
"preventFullImport": true
}
}]
]
import { map, some } from 'lodash'
// 被替换为
import map from 'lodash/map'
import some from 'lodash/some'
注意这个选项 preventFullImport 在引入整个库的时候会让插件抛出异常。
通过 ESLint 规则配置
# .eslintrc
"no-restricted-imports": [
"error",
{
"paths": [
"lodash"
]
}
]
import { map } from 'lodash'
import map from 'lodash/map'
具体使用方法可查看该规则说明,可以对引入模块的代码风格进行控制。
5.1.3 删除可替代的类库,重写方法实现
使用功能齐全的工具性函数是非常诱人的,可以快速交付,或者是能够对未来的功能进行快速实现。但是过度实现增加了目前不需要的代码,其造成的复杂性,会对 bundle 的大小产生一定的影响。在我们的项目中使用到的是 Lodash,官方虽然指出只引入对应模块就会便捷很多。但 Lodash 依然有很多存在依赖关系的内部函数需要一起打包进去。如果你仅仅是使用到这个实用库类的部分工具函数,那么可以用一些体积更小的工具包进行优化,或者直接使用对应的原生实现方式进行替换。如果我们的对于项目代码中的依赖关系,只引入了一小部分相关内容,并且可以在合理的时间内对其进行重写。那么我们应该重写这部分代码,以达到优化冗余代码的目的。把项目中涉及到的工具库类函数直接用原生代码替换,不失为一个很好的解决方案。以下是原生 JavaScript 实现 Lodash 的 debounce 函数:function debounce(func, wait, immediate) {
var timeout;
return function() {
var context = this, args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
timeout = null;
if (!immediate) func.apply(context, args);
}, wait);
if (immediate && !timeout) func.apply(context, args);
};
}
// Avoid costly calculations while the window size is in flux.
jQuery(window).on('resize', debounce(calculateLayout, 150));
这里的替换掉不必要的组件/模块,更多地是从业务逻辑方面来说的。如果已经引用的库里面存在某些业务逻辑功能,或者有公用的组件已经实现了对应的功能,那么我们应该进行替换,删除掉多余的业务内代码。同样的,检查下 package.json 文件中也许会存在未使用的包,或者是重复功能。在开发阶段,也许会存在引用了某些库类,随着业务变化,又在具体逻辑中删除了引用,但未清除彻底,导致 package 中还有残余,却给 bundle size 带来了一定的负担。也或者是同上面 lodash 和 moment 库,可以通过用一些更简单的库,或者自己实现几个常用功能来进行整个模块的替换。在这里对于我们的业务包来说,包内存在以下这些问题:2)删除不必要的 node_modules 模块,或者用其他模块替代在这个层面上来说,是细粒度上面的代码冗余的清理,包括下线实验代码的处理工作等等。
除了页面展示需要的代码模块以外,不应该加载多余的代码逻辑。对于不同的业务固然有不同的方法,但核心的两个主要方法是:
1) 使用 Ctrip React Native 的 lazyRequire 方案
React Native 官方提供的 require 目前并不支持动态加载,所以 CRN 框架提供了 lazyRequire来支持懒加载方案。App组件将在跳转页面的时候再加载该模块。let page = lazyRequire('./src/Page.js');
const pages = [
{
component: page,
name:'page',
isInitialPage: true
}
];
我们可以通过内联引用的方式,延迟模块或文件的加载,直到实际需要该文件。但如上所说,目前 React Native并不支持动态加载,所以需要 state 属性去控制是否引入对应模块。import React, { Component } from 'react';
import { TouchableOpacity, View, Text } from 'react-native';
let VeryExpensive = null;
export default class Optimized extends Component {
state = { needsExpensive: false };
didPress = () => {
if (VeryExpensive == null) {
VeryExpensive = require('./VeryExpensive').default;
}
this.setState(() => ({
needsExpensive: true,
}));
};
render() {
return (
<View style={{ marginTop: 20 }}>
<TouchableOpacity onPress={this.didPress}>
<Text>Load</Text>
</TouchableOpacity>
{this.state.needsExpensive ? <VeryExpensive /> : null}
</View>
);
}
}
5.4.1 图片资源优化
在 bundle 分析截图中,到目前为止虽然已经解决了一些库类引用不合理的问题,但还存在逻辑文件过长的问题,分析后发现是里面包含了超大的base64图片。base64 更适合出现在一些重复使用的背景图片,或者尺寸极小的 ICON 的情形,而一些较大的图片则适合使用 PNG 或者 JPEG 。PNG 是无损的,JPEG 是有损的。如果不需要背景透明,那么把 PNG 转换为 JPEG 会更节省空间。1)剪裁图片大小:设计师给出的图片一般会比较大,而实际应用中不需要这么大的图片,可以适当地进行图片大小的裁剪。2)压缩图片质量:对图片进行无损压缩后,再进行 base64 使用。经过以上两个步骤以后,base64 的图片字节数会明显减少很多。如果字节数还是很大,那么应该考虑是否不适合使用 base64 进行展示。5.4.2 ESLint 检测 React Native 的 CSS 冗余
在 React Native 的 ESLint 规则中配置 react-native/no-unused-styles ,会检测在 React 组件中存在的未使用 CSS 。在长期对组件进行开发的过程中,随着 UX/UI 的更改,会存在一些冗余的样式散落在文件中。这样的一个配置可以很好地显示出冗余的部分。# .eslintrc
"rules": {
"react-native/no-unused-styles": 2
}
但它存在很明显的缺陷,就是在 css-in-js 的写法中可以检测到当前文件中的 css 对象引用,也就是对于 styles.xxx 可以很好地检测到。
2)使用 StyleSheet.flatten 等方法操作过的 style 无法识别;const styles = StyleSheet.flatten([style1, style2])
// 无法检测到该对象中存在的样式
3)CSS 对象初始化与使用名称不同时,无法识别。
const A = StyleSheet.create({
header: {
color: "#61dafb",
fontSize: 30,
marginBottom: 36
}
})
const styles = IS_BOOL ? A : B
// 无法检测到 A/B 中存在的样式
这些问题官方目前都未修复。这个规则只能检测最简单形式的 CSS 内容中的冗余,如果希望一直能使用该规则,则在代码规范上需要保持简洁的 CSS 引用形式。
在经过一系列的分析与细节的优化操作过后,成果是压缩后的 Bundle Size 减少了约 50% 的空间占比。
进行 bundle 分析后,可以明显地找出尺寸异常的文件或者模块,进行对应的优化,从大的层面上进行分析与尺寸优化:1)根据 bundle 分析裁剪具体模块,分析模块引用代价;粗粒度的优化后,剩下的有关逻辑的代码优化,就跟平时的编写有关。从小的层面上进行优化需要:2)编写逻辑代码时,需要更加注重保持代码行数的简洁;在代码编写阶段保持最佳实践是最好的,但在中后期我们也能通过一些分析工具进行代码包的裁剪。项目结构与模块的依赖关系更加复杂的时候,运用以上方案进行 bundle 裁剪会更加有效。保持Bundle尺寸精简是一个长期的任务,就像保持代码简洁一样,甚至可以说这项任务与项目的生命周期是紧密相连的。有了良好的工具可以更加方便开发者进行分析,实时查看目前的代码简洁程度。团队招聘信息
我们是携程机票研发团队,负责携程APP/PC端机票业务开发及创新。机票研发在搜索引擎、数据库、深度学习、高并发等方向持续不断地深入探索,持续优化用户体验,提高效率。
在机票研发,你可以和众多技术顶尖大牛一起,真实的让亿万用户享受你的产品和代码,提升全球旅行者的出行体验和幸福指数。
如果你热爱技术,并渴望不断成长,携程机票研发团队期待与你一起腾飞。目前我们前端/后台/数据/测试开发等领域均有开放职位。
简历投递邮箱:tech@ctrip.com,邮件标题:【姓名】-【携程机票】-【投递职位】。